﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CSharp;

namespace DNX.IoCnoC
{   
    public interface IResolver
    {
        T Resolve<T>();
        object Resolve(Type type);
        T[] ResolveAll <T>();
        object[] ResolveAll(Type type);
        void InjectProperties(object target);
        bool Exist<T>();
        bool Exist(Type type);
    }

    public abstract class ResolverBase : IResolver
    {
        protected readonly Dictionary<Type, Func<object>> _repository = new Dictionary<Type, Func<object>>();
        
        public T Resolve<T>()
        {
            return (T)Resolve(typeof(T));
        }

        public object Resolve(Type type)
        {
            return _repository.First(i=>type.IsAssignableFrom(i.Key)).Value();
        }

        public T[] ResolveAll<T>()
        {
            return ResolveAll(typeof (T)).Cast<T>().ToArray();
        }

        public object[] ResolveAll(Type type)
        {
            return (from item in _repository
                    where type.IsAssignableFrom(item.Key)
                    select item.Value()).ToArray();
        }

        public void InjectProperties(object target)
        {
            var type = target.GetType();
            foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
                if (property.CanWrite && Exist(property.PropertyType))
                    property.SetValue(target, Resolve(property.PropertyType), null);    
                
        }

        public bool Exist<T>()
        {
            return Exist(typeof (T));
        }

        public bool Exist(Type type)
        {
            return _repository.Keys.Any(type.IsAssignableFrom);
        }
    }

    public interface IResolverBuilder
    {
        string GenerateCode();
        IResolverBuilder AddField(string name, Type type);
        IResolverBuilder AddTransientFactory(string name, Type type, Type returnType, ISourceParameter[] sourceParameters);
        object Compile(params CodeCompileUnit[] units);
        IResolverBuilder AddConstructor();
        IResolverBuilder AddInitializeField(string name, Type type, ISourceParameter sourceParameter);
        IResolverBuilder AddSingheltonFactory(string name, Type type, Type returnType, ReferenceField referenceField);
        IMethodStatement AddVariableTransientFactory(string name, Type type, Type returnType, ISourceParameter[] sourceParameters);
        IMethodStatement AddVariableTransientFactory(string name, string type, string returnType, ISourceParameter[] sourceParameters, string @namespace);
        IResolverBuilder AddProperty(string name, ISourceParameter sourceParameter);
        IResolverBuilder AddReferencedAssembly(Type type);
        IResolverBuilder AddVariableTransient(string name, Type type, ISourceParameter sourceParameter);
        IResolverBuilder AddVariableProperty(string name, ISourceParameter sourceParameter);
        IResolverBuilder AddField(string name, string typeName, string @namespace);
        IResolverBuilder AddSingheltonFactory(string name, string type, string returnType, ReferenceField referenceField, string @namespace);
        IResolverBuilder AddDelegateReference(string type, string methodName);
    }

    public class ResolverBuilder : IResolverBuilder
    {
        private const string RootNamespace = "DNX.IoConC.Dynamic.Factory";
        private readonly CodeCompileUnit targetUnit;
        private readonly CodeTypeDeclaration targetClass;
        private readonly CodeNamespace unit;
        private readonly CompilerParameters compilerParameters;
        private CodeConstructor constructor;

        public ResolverBuilder()
        {
            targetUnit = new CodeCompileUnit();
            unit = new CodeNamespace(RootNamespace);
            //unit.Imports.Add(new CodeNamespaceImport("System"));
            targetClass = new CodeTypeDeclaration("Resolver")
                {
                    IsClass = true,
                    TypeAttributes = TypeAttributes.Public
                };
            unit.Types.Add(targetClass);
            targetUnit.Namespaces.Add(unit);

            compilerParameters = new CompilerParameters
            {
                IncludeDebugInformation = true,
                GenerateInMemory = true,
                //OutputAssembly = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin", "Debug", string.Format("{0}.dll", RootNamespace)),
                OutputAssembly = Path.Combine(@"c:\\temp", string.Format("{0}.dll", RootNamespace)),
            };

            DefineResolver();
        }

        private void DefineResolver()
        {
            var type = typeof (ResolverBase);
            AddReferencedAssembly(type);
            targetClass.BaseTypes.Add(type);
        }

        public IResolverBuilder AddField(string name, Type type)
        {
            AddReferencedAssembly(type);
            AddField(name, type.FullName, type.Namespace);
            return this;
        }

        public IResolverBuilder AddDelegateReference(string type, string methodName)
        {
            constructor.Statements.Add(
                new CodeMethodInvokeExpression(
                    new CodeFieldReferenceExpression(
                        new CodeThisReferenceExpression(),
                        "_repository"),
                    "Add",
                    new CodeTypeOfExpression(type),
                    new CodeMethodReferenceExpression(new CodeThisReferenceExpression(),
                                                      methodName)));

            return this;
        }

        public IResolverBuilder AddField(string name, string typeName, string @namespace)
        {
            //unit.Imports.Add(new CodeNamespaceImport(@namespace));
            
            var field = new CodeMemberField
            {
                Attributes = MemberAttributes.Private,
                Name = name,
                Type = new CodeTypeReference(typeName)
            };
            targetClass.Members.Add(field);
            return this;
        }

        public IResolverBuilder AddReferencedAssembly(Type type)
        {
            var reference = Assembly.GetAssembly(type).ManifestModule.ScopeName;
            if (!compilerParameters.ReferencedAssemblies.Contains(reference))
                compilerParameters.ReferencedAssemblies.Add(reference);
            return this;
        }

        public IResolverBuilder AddVariableTransient(string name, Type type, ISourceParameter sourceParameter)
        {
            constructor.Statements.Add(new CodeVariableDeclarationStatement(type,name,sourceParameter.Create()));
            return this;
        }

        public IResolverBuilder AddTransientFactory(string name, Type type, Type returnType, ISourceParameter[] sourceParameters)
        {
            AddReferencedAssembly(type);
            AddReferencedAssembly(returnType);
            var method = new CodeMemberMethod
                {
                    Attributes = MemberAttributes.Public,
                    Name = name,
                    ReturnType = new CodeTypeReference(returnType)
                };
            var returnStatement = new CodeMethodReturnStatement
                {
                    Expression = CodeObjectCreateExpression(type, sourceParameters)
                };
            method.Statements.Add(returnStatement);
            //unit.Imports.Add(new CodeNamespaceImport(type.Namespace));
            targetClass.Members.Add(method);
            return this;
        }

        public IMethodStatement AddVariableTransientFactory(string name, Type type, Type returnType, ISourceParameter[] sourceParameters)
        {
            AddReferencedAssembly(type);
            AddReferencedAssembly(returnType);

            var method = new CodeMemberMethod
            {
                Attributes = MemberAttributes.Public,
                Name = name,
                ReturnType = new CodeTypeReference(returnType)
            };

            var objectCreate = CodeObjectCreateExpression(type, sourceParameters);

            var variableStatement = new CodeVariableDeclarationStatement(
                new CodeTypeReference(type.FullName), "result",
                objectCreate);
            method.Statements.Add(variableStatement);
            //unit.Imports.Add(new CodeNamespaceImport(type.Namespace));
            targetClass.Members.Add(method);
            return new MethodStatement
                {
                    CodeMember = method,
                    Builder = this
                };
        }

        public IMethodStatement AddVariableTransientFactory(string name, string type, string returnType, ISourceParameter[] sourceParameters, string @namespace)
        {
            var method = new CodeMemberMethod
            {
                Attributes = MemberAttributes.Public,
                Name = name,
                ReturnType = new CodeTypeReference(returnType)
            };

            var objectCreate = CodeObjectCreateExpression(type, sourceParameters);

            var variableStatement = new CodeVariableDeclarationStatement(
                new CodeTypeReference(type), "result",
                objectCreate);
            method.Statements.Add(variableStatement);
            //unit.Imports.Add(new CodeNamespaceImport(@namespace));
            targetClass.Members.Add(method);
            return new MethodStatement
            {
                CodeMember = method,
                Builder = this
            };
        }

        public IResolverBuilder AddProperty(string name, ISourceParameter sourceParameter)
        {
            constructor.Statements.Add(new CodeAssignStatement(
                                          new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), name),
                                          sourceParameter.Create()));
            return this;
        }

        public IResolverBuilder AddVariableProperty(string name, ISourceParameter sourceParameter)
        {
            constructor.Statements.Add(new CodeAssignStatement(
                                          new CodeVariableReferenceExpression(name), 
                                          sourceParameter.Create()));
            return this;
        }


        public IResolverBuilder AddSingheltonFactory(string name, Type type, Type returnType, ReferenceField referenceField)
        {
            AddReferencedAssembly(type);
            AddReferencedAssembly(returnType);
            AddSingheltonFactory(name, type.FullName, returnType.FullName, referenceField, type.Namespace);
            return this;
        }

        public IResolverBuilder AddSingheltonFactory(string name, string type, string returnType, ReferenceField referenceField, string @namespace)
        {

            var method = new CodeMemberMethod
            {
                Attributes = MemberAttributes.Public,
                Name = name,
                ReturnType = new CodeTypeReference(returnType)
            };

            var returnStatement = new CodeMethodReturnStatement
            {
                Expression = new CodeFieldReferenceExpression(
                    new CodeThisReferenceExpression(),
                    referenceField.Name)
            };

            method.Statements.Add(returnStatement);
            //unit.Imports.Add(new CodeNamespaceImport(@namespace));
            targetClass.Members.Add(method);
            return this;
        }

        private CodeObjectCreateExpression CodeObjectCreateExpression(Type type, IEnumerable<ISourceParameter> sourceParameters)
        {
            return CodeObjectCreateExpression(type.FullName, sourceParameters);
        }

        private CodeObjectCreateExpression CodeObjectCreateExpression(string fullName, IEnumerable<ISourceParameter> sourceParameters)
        {
            var objectCreate =
                new CodeObjectCreateExpression(
                    new CodeTypeReference(fullName),
                    sourceParameters.Select(parameter => parameter.Create()).ToArray());
            return objectCreate;
        }

        public object Compile(params CodeCompileUnit[] units)
        {
            var objects = new List<CodeCompileUnit>();
            if (units!=null)
                objects.AddRange(units);
            objects.Add(targetUnit);
            
            compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
            compilerParameters.ReferencedAssemblies.Add("System.dll");

            CodeDomProvider codeProvider = new CSharpCodeProvider();
            var compilerResults = codeProvider.CompileAssemblyFromDom(compilerParameters, objects.ToArray());
            if (compilerResults.Errors.HasErrors)
                throw new ErrorsException(
                    compilerResults.Errors.Cast<CompilerError>()
                        .Where(e => !e.IsWarning)
                        .Select(e => e.ErrorText)
                        .ToArray());
            return compilerResults.CompiledAssembly.CreateInstance(string.Format("{0}.{1}", RootNamespace, targetClass.Name));
        }

        public IResolverBuilder AddInitializeField(string name, Type type, ISourceParameter sourceParameter)
        {
            var fieldReference =
                new CodeFieldReferenceExpression(
                new CodeThisReferenceExpression(), name);
            constructor.Statements.Add(new CodeAssignStatement(fieldReference, sourceParameter.Create()));
            return this;
        }

        public IResolverBuilder AddConstructor()
        {
            constructor = new CodeConstructor {Attributes = MemberAttributes.Public};
            targetClass.Members.Add(constructor);
            return this;
        }

        public string GenerateCode()
        {
            var provider = CodeDomProvider.CreateProvider("CSharp");
            var options = new CodeGeneratorOptions {BracingStyle = "C"};
            using (var stringWriter = new StringWriter())
            {
                provider.GenerateCodeFromCompileUnit(targetUnit, stringWriter, options);
                var stringBuilder = stringWriter.GetStringBuilder();
                var autogenerated = stringWriter.ToString();
                stringBuilder.Remove(0, autogenerated.IndexOf("namespace", StringComparison.Ordinal));
                return stringWriter.ToString();
            }
        }
    }
}
}